diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py
index b16ccaa1d68ec5ab1a8ed7b2d6d011040d503e91..d05d376f67fe4b0df50e93beb54f0cd7fda40eaa 100644
--- a/homeassistant/components/spotify/__init__.py
+++ b/homeassistant/components/spotify/__init__.py
@@ -29,7 +29,7 @@ from .util import (
     spotify_uri_from_media_browser_url,
 )
 
-PLATFORMS = [Platform.MEDIA_PLAYER]
+PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR]
 
 __all__ = [
     "async_browse_media",
diff --git a/homeassistant/components/spotify/coordinator.py b/homeassistant/components/spotify/coordinator.py
index e8800220fddc4cdca0c407947dc42fc382756315..556ad88127bfc301ff9199bb3ad90c43127695ac 100644
--- a/homeassistant/components/spotify/coordinator.py
+++ b/homeassistant/components/spotify/coordinator.py
@@ -6,12 +6,14 @@ import logging
 
 from spotifyaio import (
     ContextType,
+    ItemType,
     PlaybackState,
     Playlist,
     SpotifyClient,
     SpotifyConnectionError,
     UserProfile,
 )
+from spotifyaio.models import AudioFeatures
 
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -29,6 +31,7 @@ class SpotifyCoordinatorData:
     current_playback: PlaybackState | None
     position_updated_at: datetime | None
     playlist: Playlist | None
+    audio_features: AudioFeatures | None
     dj_playlist: bool = False
 
 
@@ -53,6 +56,7 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
         )
         self.client = client
         self._playlist: Playlist | None = None
+        self._currently_loaded_track: str | None = None
 
     async def _async_setup(self) -> None:
         """Set up the coordinator."""
@@ -65,12 +69,22 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
         current = await self.client.get_playback()
         if not current:
             return SpotifyCoordinatorData(
-                current_playback=None, position_updated_at=None, playlist=None
+                current_playback=None,
+                position_updated_at=None,
+                playlist=None,
+                audio_features=None,
             )
         # Record the last updated time, because Spotify's timestamp property is unreliable
         # and doesn't actually return the fetch time as is mentioned in the API description
         position_updated_at = dt_util.utcnow()
 
+        audio_features: AudioFeatures | None = None
+        if (item := current.item) is not None and item.type == ItemType.TRACK:
+            if item.uri != self._currently_loaded_track:
+                self._currently_loaded_track = item.uri
+                audio_features = await self.client.get_audio_features(item.uri)
+            else:
+                audio_features = self.data.audio_features
         dj_playlist = False
         if (context := current.context) is not None:
             if self._playlist is None or self._playlist.uri != context.uri:
@@ -93,5 +107,6 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
             current_playback=current,
             position_updated_at=position_updated_at,
             playlist=self._playlist,
+            audio_features=audio_features,
             dj_playlist=dj_playlist,
         )
diff --git a/homeassistant/components/spotify/sensor.py b/homeassistant/components/spotify/sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..bf3fd8b07d0bedc3fdfa21c866a2117aa7ceca89
--- /dev/null
+++ b/homeassistant/components/spotify/sensor.py
@@ -0,0 +1,85 @@
+"""Sensor platform for Spotify."""
+
+from collections.abc import Callable
+from dataclasses import dataclass
+
+from spotifyaio.models import AudioFeatures
+
+from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from . import DOMAIN, SpotifyConfigEntry
+from .coordinator import SpotifyCoordinator
+
+
+@dataclass(frozen=True, kw_only=True)
+class SpotifyAudioFeaturesSensorEntityDescription(SensorEntityDescription):
+    """Describes Spotify sensor entity."""
+
+    value_fn: Callable[[AudioFeatures], float]
+
+
+AUDIO_FEATURE_SENSORS: tuple[SpotifyAudioFeaturesSensorEntityDescription, ...] = (
+    SpotifyAudioFeaturesSensorEntityDescription(
+        key="bpm",
+        translation_key="song_tempo",
+        native_unit_of_measurement="bpm",
+        suggested_display_precision=0,
+        value_fn=lambda audio_features: audio_features.tempo,
+    ),
+)
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: SpotifyConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up Spotify sensor based on a config entry."""
+    coordinator = entry.runtime_data.coordinator
+
+    user_id = entry.unique_id
+
+    assert user_id is not None
+
+    async_add_entities(
+        SpotifyAudioFeatureSensor(coordinator, description, user_id, entry.title)
+        for description in AUDIO_FEATURE_SENSORS
+    )
+
+
+class SpotifyAudioFeatureSensor(CoordinatorEntity[SpotifyCoordinator], SensorEntity):
+    """Representation of a Spotify sensor."""
+
+    _attr_has_entity_name = True
+    entity_description: SpotifyAudioFeaturesSensorEntityDescription
+
+    def __init__(
+        self,
+        coordinator: SpotifyCoordinator,
+        entity_description: SpotifyAudioFeaturesSensorEntityDescription,
+        user_id: str,
+        name: str,
+    ) -> None:
+        """Initialize."""
+        super().__init__(coordinator)
+        self._attr_unique_id = f"{user_id}_{entity_description.key}"
+        self.entity_description = entity_description
+        self._attr_device_info = DeviceInfo(
+            identifiers={(DOMAIN, user_id)},
+            manufacturer="Spotify AB",
+            model=f"Spotify {coordinator.current_user.product}",
+            name=f"Spotify {name}",
+            entry_type=DeviceEntryType.SERVICE,
+            configuration_url="https://open.spotify.com",
+        )
+
+    @property
+    def native_value(self) -> float | None:
+        """Return the state of the sensor."""
+        if (audio_features := self.coordinator.data.audio_features) is None:
+            return None
+        return self.entity_description.value_fn(audio_features)
diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json
index 90e573a1706088a7cb57b2f4858013409ef2130f..d98e70b9fe16b91bd45a4809bd35d54b1b59b38c 100644
--- a/homeassistant/components/spotify/strings.json
+++ b/homeassistant/components/spotify/strings.json
@@ -30,5 +30,12 @@
     "info": {
       "api_endpoint_reachable": "Spotify API endpoint reachable"
     }
+  },
+  "entity": {
+    "sensor": {
+      "song_tempo": {
+        "name": "Song tempo"
+      }
+    }
   }
 }
diff --git a/tests/components/spotify/conftest.py b/tests/components/spotify/conftest.py
index d8e11d66ad14a3e66ef1208ba681ecaf94c2b70e..5d86045e5a846132c4a408d2634665ecf7128480 100644
--- a/tests/components/spotify/conftest.py
+++ b/tests/components/spotify/conftest.py
@@ -9,6 +9,7 @@ from spotifyaio.models import (
     Album,
     Artist,
     ArtistResponse,
+    AudioFeatures,
     CategoriesResponse,
     Category,
     CategoryPlaylistResponse,
@@ -132,6 +133,7 @@ def mock_spotify() -> Generator[AsyncMock]:
             ("album.json", "get_album", Album),
             ("artist.json", "get_artist", Artist),
             ("show.json", "get_show", Show),
+            ("audio_features.json", "get_audio_features", AudioFeatures),
         ):
             getattr(client, method).return_value = obj.from_json(
                 load_fixture(fixture, DOMAIN)
diff --git a/tests/components/spotify/fixtures/audio_features.json b/tests/components/spotify/fixtures/audio_features.json
new file mode 100644
index 0000000000000000000000000000000000000000..1263d231f5eea899630317692f431cb175771b4b
--- /dev/null
+++ b/tests/components/spotify/fixtures/audio_features.json
@@ -0,0 +1,20 @@
+{
+  "danceability": 0.696,
+  "energy": 0.905,
+  "key": 2,
+  "loudness": -2.743,
+  "mode": 1,
+  "speechiness": 0.103,
+  "acousticness": 0.011,
+  "instrumentalness": 0.000905,
+  "liveness": 0.302,
+  "valence": 0.625,
+  "tempo": 114.944,
+  "type": "audio_features",
+  "id": "11dFghVXANMlKmJXsNCbNl",
+  "uri": "spotify:track:11dFghVXANMlKmJXsNCbNl",
+  "track_href": "https://api.spotify.com/v1/tracks/11dFghVXANMlKmJXsNCbNl",
+  "analysis_url": "https://api.spotify.com/v1/audio-analysis/11dFghVXANMlKmJXsNCbNl",
+  "duration_ms": 207960,
+  "time_signature": 4
+}
diff --git a/tests/components/spotify/snapshots/test_diagnostics.ambr b/tests/components/spotify/snapshots/test_diagnostics.ambr
index 40502562da36f2cae2a7c2560b6a1b55e81d2a8e..264f99bed60ba59f660d012106e1937ff311be2e 100644
--- a/tests/components/spotify/snapshots/test_diagnostics.ambr
+++ b/tests/components/spotify/snapshots/test_diagnostics.ambr
@@ -14,6 +14,20 @@
       }),
     ]),
     'playback': dict({
+      'audio_features': dict({
+        'acousticness': 0.011,
+        'danceability': 0.696,
+        'energy': 0.905,
+        'instrumentalness': 0.000905,
+        'key': 2,
+        'liveness': 0.302,
+        'loudness': -2.743,
+        'mode': 1,
+        'speechiness': 0.103,
+        'tempo': 114.944,
+        'time_signature': 4,
+        'valence': 0.625,
+      }),
       'current_playback': dict({
         'context': dict({
           'context_type': 'playlist',
diff --git a/tests/components/spotify/snapshots/test_sensor.ambr b/tests/components/spotify/snapshots/test_sensor.ambr
new file mode 100644
index 0000000000000000000000000000000000000000..5c99c878286103ee7e4a9fd5cfd4afd28dfbc353
--- /dev/null
+++ b/tests/components/spotify/snapshots/test_sensor.ambr
@@ -0,0 +1,51 @@
+# serializer version: 1
+# name: test_entities[sensor.spotify_spotify_1_song_tempo-entry]
+  EntityRegistryEntrySnapshot({
+    'aliases': set({
+    }),
+    'area_id': None,
+    'capabilities': None,
+    'config_entry_id': <ANY>,
+    'device_class': None,
+    'device_id': <ANY>,
+    'disabled_by': None,
+    'domain': 'sensor',
+    'entity_category': None,
+    'entity_id': 'sensor.spotify_spotify_1_song_tempo',
+    'has_entity_name': True,
+    'hidden_by': None,
+    'icon': None,
+    'id': <ANY>,
+    'labels': set({
+    }),
+    'name': None,
+    'options': dict({
+      'sensor': dict({
+        'suggested_display_precision': 0,
+      }),
+    }),
+    'original_device_class': None,
+    'original_icon': None,
+    'original_name': 'Song tempo',
+    'platform': 'spotify',
+    'previous_unique_id': None,
+    'supported_features': 0,
+    'translation_key': 'song_tempo',
+    'unique_id': '1112264111_bpm',
+    'unit_of_measurement': 'bpm',
+  })
+# ---
+# name: test_entities[sensor.spotify_spotify_1_song_tempo-state]
+  StateSnapshot({
+    'attributes': ReadOnlyDict({
+      'friendly_name': 'Spotify spotify_1 Song tempo',
+      'unit_of_measurement': 'bpm',
+    }),
+    'context': <ANY>,
+    'entity_id': 'sensor.spotify_spotify_1_song_tempo',
+    'last_changed': <ANY>,
+    'last_reported': <ANY>,
+    'last_updated': <ANY>,
+    'state': '114.944',
+  })
+# ---
diff --git a/tests/components/spotify/test_media_player.py b/tests/components/spotify/test_media_player.py
index cc8526d1cf5a40e3c38c4aa7e40aaf35ad534f3e..b03424f8459cee6400472d1b0c03c2c94cafb93a 100644
--- a/tests/components/spotify/test_media_player.py
+++ b/tests/components/spotify/test_media_player.py
@@ -45,6 +45,7 @@ from homeassistant.const import (
     SERVICE_SHUFFLE_SET,
     SERVICE_VOLUME_SET,
     STATE_UNAVAILABLE,
+    Platform,
 )
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import entity_registry as er
@@ -70,7 +71,10 @@ async def test_entities(
 ) -> None:
     """Test the Spotify entities."""
     freezer.move_to("2023-10-21")
-    with patch("secrets.token_hex", return_value="mock-token"):
+    with (
+        patch("secrets.token_hex", return_value="mock-token"),
+        patch("homeassistant.components.spotify.PLATFORMS", [Platform.MEDIA_PLAYER]),
+    ):
         await setup_integration(hass, mock_config_entry)
 
         await snapshot_platform(
@@ -92,7 +96,10 @@ async def test_podcast(
     mock_spotify.return_value.get_playback.return_value = PlaybackState.from_json(
         load_fixture("playback_episode.json", DOMAIN)
     )
-    with patch("secrets.token_hex", return_value="mock-token"):
+    with (
+        patch("secrets.token_hex", return_value="mock-token"),
+        patch("homeassistant.components.spotify.PLATFORMS", [Platform.MEDIA_PLAYER]),
+    ):
         await setup_integration(hass, mock_config_entry)
 
         await snapshot_platform(
diff --git a/tests/components/spotify/test_sensor.py b/tests/components/spotify/test_sensor.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5fd2389e69fe270409814f3971d449627f564ea
--- /dev/null
+++ b/tests/components/spotify/test_sensor.py
@@ -0,0 +1,65 @@
+"""Tests for the Spotify sensor platform."""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+from spotifyaio import PlaybackState
+from syrupy import SnapshotAssertion
+
+from homeassistant.components.spotify import DOMAIN
+from homeassistant.const import STATE_UNKNOWN, Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from . import setup_integration
+
+from tests.common import MockConfigEntry, load_fixture, snapshot_platform
+
+
+@pytest.mark.usefixtures("setup_credentials")
+async def test_entities(
+    hass: HomeAssistant,
+    mock_spotify: MagicMock,
+    mock_config_entry: MockConfigEntry,
+    entity_registry: er.EntityRegistry,
+    snapshot: SnapshotAssertion,
+) -> None:
+    """Test the Spotify entities."""
+    with patch("homeassistant.components.spotify.PLATFORMS", [Platform.SENSOR]):
+        await setup_integration(hass, mock_config_entry)
+
+    await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
+
+
+@pytest.mark.usefixtures("setup_credentials")
+async def test_audio_features_unavailable(
+    hass: HomeAssistant,
+    mock_spotify: MagicMock,
+    mock_config_entry: MockConfigEntry,
+    entity_registry: er.EntityRegistry,
+    snapshot: SnapshotAssertion,
+) -> None:
+    """Test the Spotify entities."""
+    mock_spotify.return_value.get_audio_features.return_value = None
+
+    await setup_integration(hass, mock_config_entry)
+
+    assert hass.states.get("sensor.spotify_spotify_1_song_tempo").state == STATE_UNKNOWN
+
+
+@pytest.mark.usefixtures("setup_credentials")
+async def test_audio_features_unknown_during_podcast(
+    hass: HomeAssistant,
+    mock_spotify: MagicMock,
+    mock_config_entry: MockConfigEntry,
+    entity_registry: er.EntityRegistry,
+    snapshot: SnapshotAssertion,
+) -> None:
+    """Test the Spotify audio features sensor during a podcast."""
+    mock_spotify.return_value.get_playback.return_value = PlaybackState.from_json(
+        load_fixture("playback_episode.json", DOMAIN)
+    )
+
+    await setup_integration(hass, mock_config_entry)
+
+    assert hass.states.get("sensor.spotify_spotify_1_song_tempo").state == STATE_UNKNOWN