diff --git a/README.rst b/README.rst
index a7a52be..1fcb7c7 100644
--- a/README.rst
+++ b/README.rst
@@ -12,9 +12,9 @@ extensions.
 - Python 2.7 and 3.4+ are supported.
 
 - Since threads aren't appropriate to every situation, it doesn't
-  require threads. It takes care not to forbid them, either. The same
-  goes for greenlets, callbacks, continuations and generators. It is
-  not necessarily thread-safe however, and your mileage will vary.
+  require threads. Pika core takes care not to forbid them, either. The same
+  goes for greenlets, callbacks, continuations, and generators. An instance of
+  Pika's built-in connection adapters is not thread-safe, however.
 
 - People may be using direct sockets, plain old `select()`,
   or any of the wide variety of ways of getting network events to and from a
diff --git a/pika/heartbeat.py b/pika/heartbeat.py
index af6d93c..a3822a4 100644
--- a/pika/heartbeat.py
+++ b/pika/heartbeat.py
@@ -50,7 +50,7 @@ class HeartbeatChecker(object):
         :rtype True
 
         """
-        return self._connection.heartbeat is self
+        return self._connection._heartbeat_checker is self
 
     @property
     def bytes_received_on_connection(self):

BlockingConnection heartbeat attribute can be undefined
I've been testing connection recovery docs and managed to run into the following exceptions several times:

```
Traceback (most recent call last):
  File "./blocking_consumer_recovery2.py", line 31, in <module>
    channel.start_consuming()
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/adapters/blocking_connection.py", line 1878, in start_consuming
    self._process_data_events(time_limit=None)
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/adapters/blocking_connection.py", line 2040, in _process_data_events
    self.connection.process_data_events(time_limit=time_limit)
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/adapters/blocking_connection.py", line 814, in process_data_events
    self._flush_output(common_terminator)
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/adapters/blocking_connection.py", line 525, in _flush_output
    self._impl.ioloop.process_timeouts()
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/adapters/select_connection.py", line 462, in process_timeouts
    self._timer.process_timeouts()
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/adapters/select_connection.py", line 297, in process_timeouts
    timeout.callback()
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/heartbeat.py", line 104, in send_and_check
    self._start_timer()
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/heartbeat.py", line 167, in _start_timer
    if self.active:
  File "/Users/antares/Development/RabbitMQ/pika.git/pika/heartbeat.py", line 53, in active
    return self._connection.heartbeat is self
AttributeError: 'SelectConnection' object has no attribute 'heartbeat'
```

unfortunately I don't have a 100% clear idea of how to reproduce this but all I did was restarting down nodes cleanly. At some point during recovery attempt the above exception would make the process exit. Perhaps the code that checks for `self._connection.heartbeat` should be more defensive.
diff --git a/tests/unit/heartbeat_tests.py b/tests/unit/heartbeat_tests.py
index c9e8b63..2ff98a7 100644
--- a/tests/unit/heartbeat_tests.py
+++ b/tests/unit/heartbeat_tests.py
@@ -20,15 +20,39 @@ import pika.exceptions
 # pylint: disable=C0103
 
 
+class ConstructableConnection(connection.Connection):
+    """Adds dummy overrides for `Connection`'s abstract methods so
+    that we can instantiate and test it.
+
+    """
+    def _adapter_connect_stream(self):
+        pass
+
+    def _adapter_disconnect_stream(self):
+        raise NotImplementedError
+
+    def add_timeout(self, deadline, callback):
+        raise NotImplementedError
+
+    def remove_timeout(self, timeout_id):
+        raise NotImplementedError
+
+    def _adapter_emit_data(self, data):
+        raise NotImplementedError
+
+    def _adapter_get_write_buffer_size(self):
+        raise NotImplementedError
+
+
 class HeartbeatTests(unittest.TestCase):
 
     INTERVAL = 5
 
     def setUp(self):
-        self.mock_conn = mock.Mock(spec=connection.Connection)
+        self.mock_conn = mock.Mock(spec_set=ConstructableConnection())
         self.mock_conn.bytes_received = 100
         self.mock_conn.bytes_sent = 100
-        self.mock_conn.heartbeat = mock.Mock(spec=heartbeat.HeartbeatChecker)
+        self.mock_conn._heartbeat_checker = mock.Mock(spec=heartbeat.HeartbeatChecker)
         self.obj = heartbeat.HeartbeatChecker(self.mock_conn, self.INTERVAL)
 
     def tearDown(self):
@@ -65,11 +89,11 @@ class HeartbeatTests(unittest.TestCase):
         timer.assert_called_once_with()
 
     def test_active_true(self):
-        self.mock_conn.heartbeat = self.obj
+        self.mock_conn._heartbeat_checker = self.obj
         self.assertTrue(self.obj.active)
 
     def test_active_false(self):
-        self.mock_conn.heartbeat = mock.Mock()
+        self.mock_conn._heartbeat_checker = mock.Mock()
         self.assertFalse(self.obj.active)
 
     def test_bytes_received_on_connection(self):
@@ -178,7 +202,7 @@ class HeartbeatTests(unittest.TestCase):
 
     @mock.patch('pika.heartbeat.HeartbeatChecker._setup_timer')
     def test_start_timer_active(self, setup_timer):
-        self.mock_conn.heartbeat = self.obj
+        self.mock_conn._heartbeat_checker = self.obj
         self.obj._start_timer()
         self.assertTrue(setup_timer.called)
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
test-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==25.3.0
Automat==24.8.1
certifi==2025.1.31
charset-normalizer==3.4.1
codecov==2.1.13
constantly==23.10.4
coverage==7.8.0
exceptiongroup==1.2.2
hyperlink==21.0.0
idna==3.10
incremental==24.7.2
iniconfig==2.1.0
mock==5.2.0
nose==1.3.7
packaging==24.2
-e git+https://github.com/pika/pika.git@d9a1baa40b162cc4c638f95a8e4e9ab666af4288#egg=pika
pluggy==1.5.0
pytest==8.3.5
requests==2.32.3
tomli==2.2.1
tornado==6.4.2
Twisted==24.11.0
typing_extensions==4.13.0
urllib3==2.3.0
zope.interface==7.2

name: pika
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==25.3.0
      - automat==24.8.1
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - constantly==23.10.4
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - hyperlink==21.0.0
      - idna==3.10
      - incremental==24.7.2
      - iniconfig==2.1.0
      - mock==5.2.0
      - nose==1.3.7
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - requests==2.32.3
      - tomli==2.2.1
      - tornado==6.4.2
      - twisted==24.11.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - zope-interface==7.2
prefix: /opt/conda/envs/pika

tests/unit/heartbeat_tests.py::HeartbeatTests::test_active_false tests/unit/heartbeat_tests.py::HeartbeatTests::test_active_true tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_and_check_increment_bytes tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_and_check_increment_no_bytes tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_and_check_not_closed tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_and_check_send_heartbeat_frame tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_and_check_update_counters tests/unit/heartbeat_tests.py::HeartbeatTests::test_start_timer_active tests/unit/heartbeat_tests.py::HeartbeatTests::test_start_timer_not_active
tests/unit/heartbeat_tests.py::HeartbeatTests::test_bytes_received_on_connection tests/unit/heartbeat_tests.py::HeartbeatTests::test_connection_close tests/unit/heartbeat_tests.py::HeartbeatTests::test_connection_is_idle_false tests/unit/heartbeat_tests.py::HeartbeatTests::test_connection_is_idle_true tests/unit/heartbeat_tests.py::HeartbeatTests::test_constructor_assignment_connection tests/unit/heartbeat_tests.py::HeartbeatTests::test_constructor_assignment_heartbeat_interval tests/unit/heartbeat_tests.py::HeartbeatTests::test_constructor_called_setup_timer tests/unit/heartbeat_tests.py::HeartbeatTests::test_constructor_initial_bytes_received tests/unit/heartbeat_tests.py::HeartbeatTests::test_constructor_initial_bytes_sent tests/unit/heartbeat_tests.py::HeartbeatTests::test_constructor_initial_heartbeat_frames_received tests/unit/heartbeat_tests.py::HeartbeatTests::test_constructor_initial_heartbeat_frames_sent tests/unit/heartbeat_tests.py::HeartbeatTests::test_constructor_initial_idle_byte_intervals tests/unit/heartbeat_tests.py::HeartbeatTests::test_default_initialization_max_idle_count tests/unit/heartbeat_tests.py::HeartbeatTests::test_has_received_data_false tests/unit/heartbeat_tests.py::HeartbeatTests::test_has_received_data_true tests/unit/heartbeat_tests.py::HeartbeatTests::test_new_heartbeat_frame tests/unit/heartbeat_tests.py::HeartbeatTests::test_received tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_and_check_missed_bytes tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_and_check_start_timer tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_heartbeat_counter_incremented tests/unit/heartbeat_tests.py::HeartbeatTests::test_send_heartbeat_send_frame_called tests/unit/heartbeat_tests.py::HeartbeatTests::test_setup_timer_called tests/unit/heartbeat_tests.py::HeartbeatTests::test_update_counters_bytes_received tests/unit/heartbeat_tests.py::HeartbeatTests::test_update_counters_bytes_sent
README.rst pika/heartbeat.py
README.rst pika/heartbeat.py